/*
 * Released under BSD License
 * Copyright (c) 2014-2021 hizzgdev@163.com
 *
 * Project Home:
 *   https://github.com/hizzgdev/jsmind/
 */

;(function ($w) {
	'use strict'
	// set 'jsMind' as the library name.
	// __name__ should be a const value, Never try to change it easily.
	var __name__ = 'jsMind'
	// library version
	var __version__ = '0.4.6'
	// author
	var __author__ = 'hizzgdev@163.com'

	// an noop function define
	var _noop = function () {}
	var logger =
		typeof console === 'undefined'
			? {
					log: _noop,
					debug: _noop,
					error: _noop,
					warn: _noop,
					info: _noop
			  }
			: console

	// check global variables
	if (typeof module === 'undefined' || !module.exports) {
		if (typeof $w[__name__] != 'undefined') {
			logger.log(__name__ + ' has been already exist.')
			return
		}
	}

	// shortcut of methods in dom
	var $d = $w.document
	var $g = function (id) {
		return $d.getElementById(id)
	}
	var $c = function (tag) {
		return $d.createElement(tag)
	}
	var $t = function (n, t) {
		if (n.hasChildNodes()) {
			n.firstChild.nodeValue = t
		} else {
			n.appendChild($d.createTextNode(t))
		}
	}

	var $h = function (n, t) {
		if (t instanceof HTMLElement) {
			n.innerHTML = ''
			n.appendChild(t)
		} else {
			n.innerHTML = t
		}
	}
	// detect isElement
	var $i = function (el) {
		return (
			!!el &&
			typeof el === 'object' &&
			el.nodeType === 1 &&
			typeof el.style === 'object' &&
			typeof el.ownerDocument === 'object'
		)
	}
	if (typeof String.prototype.startsWith != 'function') {
		String.prototype.startsWith = function (p) {
			return this.slice(0, p.length) === p
		}
	}

	var DEFAULT_OPTIONS = {
		container: '', // id of the container
		editable: false, // you can change it in your options
		theme: null,
		mode: 'full', // full or side
		support_html: true,
		view: {
			engine: 'canvas',
			hmargin: 10,
			vmargin: 10,
			line_width: 2,
			line_color: '#555'
		},
		layout: {
			hspace: 30,
			vspace: 20,
			pspace: 13
		},
		default_event_handle: {
			enable_mousedown_handle: true,
			enable_click_handle: true,
			enable_dblclick_handle: true
		},
		shortcut: {
			enable: true,
			handles: {},
			mapping: {
				addchild: 45, // Insert
				addbrother: 13, // Enter
				editnode: 113, // F2
				delnode: 46, // Delete
				toggle: 32, // Space
				left: 37, // Left
				up: 38, // Up
				right: 39, // Right
				down: 40 // Down
			}
		}
	}

	// core object
	var jm = function (options) {
		jm.current = this

		this.version = __version__
		var opts = {}
		jm.util.json.merge(opts, DEFAULT_OPTIONS)
		jm.util.json.merge(opts, options)

		if (!opts.container) {
			logger.error('the options.container should not be null or empty.')
			return
		}
		this.options = opts
		this.inited = false
		this.mind = null
		this.event_handles = []
		this.init()
	}

	// ============= static object =============================================
	jm.direction = { left: -1, center: 0, right: 1 }
	jm.event_type = { show: 1, resize: 2, edit: 3, select: 4 }
	jm.key = { meta: 1 << 13, ctrl: 1 << 12, alt: 1 << 11, shift: 1 << 10 }

	jm.node = function (sId, iIndex, sTopic, oData, bIsRoot, oParent, eDirection, bExpanded) {
		if (!sId) {
			logger.error('invalid nodeid')
			return
		}
		if (typeof iIndex != 'number') {
			logger.error('invalid node index')
			return
		}
		if (typeof bExpanded === 'undefined') {
			bExpanded = true
		}
		this.id = sId
		this.index = iIndex
		this.noteName = sTopic
		this.data = oData || {}
		this.isroot = bIsRoot
		this.parent = oParent
		this.direction = eDirection
		this.expanded = !!bExpanded
		this.children = []
		this._data = {}
	}

	jm.node.compare = function (node1, node2) {
		// '-1' is alwary the last
		var r = 0
		var i1 = node1.index
		var i2 = node2.index
		if (i1 >= 0 && i2 >= 0) {
			r = i1 - i2
		} else if (i1 == -1 && i2 == -1) {
			r = 0
		} else if (i1 == -1) {
			r = 1
		} else if (i2 == -1) {
			r = -1
		} else {
			r = 0
		}
		//logger.debug(i1+' <> '+i2+'  =  '+r);
		return r
	}

	jm.node.inherited = function (pnode, node) {
		if (!!pnode && !!node) {
			if (pnode.id === node.id) {
				return true
			}
			if (pnode.isroot) {
				return true
			}
			var pid = pnode.id
			var p = node
			while (!p.isroot) {
				p = p.parent
				if (p.id === pid) {
					return true
				}
			}
		}
		return false
	}

	jm.node.prototype = {
		get_location: function () {
			var vd = this._data.view
			return {
				x: vd.abs_x,
				y: vd.abs_y
			}
		},
		get_size: function () {
			var vd = this._data.view
			return {
				w: vd.width,
				h: vd.height
			}
		}
	}

	jm.mind = function () {
		this.name = null
		this.author = null
		this.version = null
		this.root = null
		this.selected = null
		this.nodes = {}
	}

	jm.mind.prototype = {
		get_node: function (nodeVal) {
			// !changed
			const nodeArr = Object.values(this.nodes).filter(item => {
				return item.data.idLevel === nodeVal
			})

			if (!!nodeArr.length) {
				return this.nodes[nodeArr[0].id]
			} else {
				logger.warn('the node[id=' + nodeArr[0].id + '] can not be found')
				return null
			}
		},

		set_root: function (nodeid, noteName, data) {
			if (this.root == null) {
				this.root = new jm.node(nodeid, 0, noteName, data, true)
				this._put_node(this.root)
			} else {
				logger.error('root node is already exist')
			}
		},

		add_node: function (parent_node, nodeid, noteName, data, idx, direction, expanded) {
			if (!jm.util.is_node(parent_node)) {
				var the_parent_node = this.get_node(parent_node)
				if (!the_parent_node) {
					logger.error('the parent_node[id=' + parent_node + '] can not be found.')
					return null
				} else {
					return this.add_node(the_parent_node, nodeid, noteName, data, idx, direction, expanded)
				}
			}
			var nodeindex = idx || -1
			var node = null
			if (parent_node.isroot) {
				var d = jm.direction.right
				if (isNaN(direction)) {
					var children = parent_node.children
					var children_len = children.length
					var r = 0
					for (var i = 0; i < children_len; i++) {
						if (children[i].direction === jm.direction.left) {
							r--
						} else {
							r++
						}
					}
					d = children_len > 1 && r > 0 ? jm.direction.left : jm.direction.right
				} else {
					d = direction != jm.direction.left ? jm.direction.right : jm.direction.left
				}
				node = new jm.node(nodeid, nodeindex, noteName, data, false, parent_node, d, expanded)
			} else {
				node = new jm.node(
					nodeid,
					nodeindex,
					noteName,
					data,
					false,
					parent_node,
					parent_node.direction,
					expanded
				)
			}
			if (this._put_node(node)) {
				parent_node.children.push(node)
				this._reindex(parent_node)
			} else {
				logger.error("fail, the nodeid '" + node.id + "' has been already exist.")
				node = null
			}
			return node
		},

		insert_node_before: function (node_before, nodeid, noteName, data) {
			if (!jm.util.is_node(node_before)) {
				var the_node_before = this.get_node(node_before)
				if (!the_node_before) {
					logger.error('the node_before[id=' + node_before + '] can not be found.')
					return null
				} else {
					return this.insert_node_before(the_node_before, nodeid, noteName, data)
				}
			}
			var node_index = node_before.index - 0.5
			return this.add_node(node_before.parent, nodeid, noteName, data, node_index)
		},

		get_node_before: function (node) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return null
				} else {
					return this.get_node_before(the_node)
				}
			}
			if (node.isroot) {
				return null
			}
			var idx = node.index - 2
			if (idx >= 0) {
				return node.parent.children[idx]
			} else {
				return null
			}
		},

		insert_node_after: function (node_after, nodeid, noteName, data) {
			if (!jm.util.is_node(node_after)) {
				var the_node_after = this.get_node(node_after)
				if (!the_node_after) {
					logger.error('the node_after[id=' + node_after + '] can not be found.')
					return null
				} else {
					return this.insert_node_after(the_node_after, nodeid, noteName, data)
				}
			}
			var node_index = node_after.index + 0.5
			return this.add_node(node_after.parent, nodeid, noteName, data, node_index)
		},

		get_node_after: function (node) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return null
				} else {
					return this.get_node_after(the_node)
				}
			}
			if (node.isroot) {
				return null
			}
			var idx = node.index
			var brothers = node.parent.children
			if (brothers.length >= idx) {
				return node.parent.children[idx]
			} else {
				return null
			}
		},

		move_node: function (node, beforeid, parentid, direction) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return null
				} else {
					return this.move_node(the_node, beforeid, parentid, direction)
				}
			}
			if (!parentid) {
				parentid = node.parent.id
			}
			return this._move_node(node, beforeid, parentid, direction)
		},

		_flow_node_direction: function (node, direction) {
			if (typeof direction === 'undefined') {
				direction = node.direction
			} else {
				node.direction = direction
			}
			var len = node.children.length
			while (len--) {
				this._flow_node_direction(node.children[len], direction)
			}
		},

		_move_node_internal: function (node, beforeid) {
			if (!!node && !!beforeid) {
				if (beforeid == '_last_') {
					node.index = -1
					this._reindex(node.parent)
				} else if (beforeid == '_first_') {
					node.index = 0
					this._reindex(node.parent)
				} else {
					var node_before = !!beforeid ? this.get_node(beforeid) : null
					if (
						node_before != null &&
						node_before.parent != null &&
						node_before.parent.id == node.parent.id
					) {
						node.index = node_before.index - 0.5
						this._reindex(node.parent)
					}
				}
			}
			return node
		},

		_move_node: function (node, beforeid, parentid, direction) {
			if (!!node && !!parentid) {
				if (node.parent.id != parentid) {
					// remove from parent's children
					var sibling = node.parent.children
					var si = sibling.length
					while (si--) {
						if (sibling[si].id == node.id) {
							sibling.splice(si, 1)
							break
						}
					}
					node.parent = this.get_node(parentid)
					node.parent.children.push(node)
				}

				if (node.parent.isroot) {
					if (direction == jm.direction.left) {
						node.direction = direction
					} else {
						node.direction = jm.direction.right
					}
				} else {
					node.direction = node.parent.direction
				}
				this._move_node_internal(node, beforeid)
				this._flow_node_direction(node)
			}
			return node
		},

		remove_node: function (node) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return false
				} else {
					return this.remove_node(the_node)
				}
			}
			if (!node) {
				logger.error('fail, the node can not be found')
				return false
			}
			if (node.isroot) {
				logger.error('fail, can not remove root node')
				return false
			}
			if (this.selected != null && this.selected.id == node.id) {
				this.selected = null
			}
			// clean all subordinate nodes
			var children = node.children
			var ci = children.length
			while (ci--) {
				this.remove_node(children[ci])
			}
			// clean all children
			children.length = 0
			// remove from parent's children
			var sibling = node.parent.children
			var si = sibling.length
			while (si--) {
				if (sibling[si].id == node.id) {
					sibling.splice(si, 1)
					break
				}
			}
			// remove from global nodes
			delete this.nodes[node.id]
			// clean all properties
			for (var k in node) {
				delete node[k]
			}
			// remove it's self
			node = null
			//delete node;
			return true
		},

		_put_node: function (node) {
			if (node.id in this.nodes) {
				logger.warn("the nodeid '" + node.id + "' has been already exist.")
				return false
			} else {
				this.nodes[node.id] = node
				return true
			}
		},

		_reindex: function (node) {
			if (node instanceof jm.node) {
				node.children.sort(jm.node.compare)
				for (var i = 0; i < node.children.length; i++) {
					node.children[i].index = i + 1
				}
			}
		}
	}

	jm.format = {
		node_tree: {
			example: {
				meta: {
					name: __name__,
					author: __author__,
					version: __version__
				},
				format: 'node_tree',
				data: { id: 'root', noteName: 'jsMind Example' }
			},
			get_mind: function (source) {
				var df = jm.format.node_tree
				var mind = new jm.mind()
				mind.name = source.meta.name
				mind.author = source.meta.author
				mind.version = source.meta.version
				df._parse(mind, source.data)
				return mind
			},
			get_data: function (mind) {
				var df = jm.format.node_tree
				var json = {}
				json.meta = {
					name: mind.name,
					author: mind.author,
					version: mind.version
				}
				json.format = 'node_tree'
				json.data = df._buildnode(mind.root)
				return json
			},

			_parse: function (mind, node_root) {
				var df = jm.format.node_tree
				var data = df._extract_data(node_root)
				mind.set_root(node_root.id, node_root.noteName, data)
				if ('children' in node_root) {
					var children = node_root.children
					for (var i = 0; i < children.length; i++) {
						df._extract_subnode(mind, mind.root, children[i])
					}
				}
			},

			_extract_data: function (node_json) {
				var data = {}
				for (var k in node_json) {
					if (
						k == 'id' ||
						k == 'noteName' ||
						k == 'children' ||
						k == 'direction' ||
						k == 'expanded'
					) {
						continue
					}
					data[k] = node_json[k]
				}
				return data
			},

			_extract_subnode: function (mind, node_parent, node_json) {
				var df = jm.format.node_tree
				var data = df._extract_data(node_json)
				var d = null
				if (node_parent.isroot) {
					d = node_json.direction == 'left' ? jm.direction.left : jm.direction.right
				}
				var node = mind.add_node(
					node_parent,
					node_json.id,
					node_json.noteName,
					data,
					null,
					d,
					node_json.expanded
				)
				if (!!node_json['children']) {
					var children = node_json.children
					for (var i = 0; i < children.length; i++) {
						df._extract_subnode(mind, node, children[i])
					}
				}
			},

			_buildnode: function (node) {
				var df = jm.format.node_tree
				if (!(node instanceof jm.node)) {
					return
				}
				var o = {
					id: node.id,
					noteName: node.noteName,
					fraction: node.data['fraction'],
					expanded: node.expanded
				}
				if (!!node.parent && node.parent.isroot) {
					o.direction = node.direction == jm.direction.left ? 'left' : 'right'
				}
				if (node.data != null) {
					var node_data = node.data
					for (var k in node_data) {
						o[k] = node_data[k]
					}
				}
				var children = node.children
				if (children.length > 0) {
					o.children = []
					for (var i = 0; i < children.length; i++) {
						o.children.push(df._buildnode(children[i]))
					}
				}
				return o
			}
		},

		node_array: {
			example: {
				meta: {
					name: __name__,
					author: __author__,
					version: __version__
				},
				format: 'node_array',
				data: [{ id: 'root', noteName: 'jsMind Example', isroot: true }]
			},

			get_mind: function (source) {
				var df = jm.format.node_array
				var mind = new jm.mind()
				mind.name = source.meta.name
				mind.author = source.meta.author
				mind.version = source.meta.version
				df._parse(mind, source.data)
				return mind
			},

			get_data: function (mind) {
				var df = jm.format.node_array
				var json = {}
				json.meta = {
					name: mind.name,
					author: mind.author,
					version: mind.version
				}
				json.format = 'node_array'
				json.data = []
				df._array(mind, json.data)
				return json
			},

			_parse: function (mind, node_array) {
				var df = jm.format.node_array
				var narray = node_array.slice(0)
				// reverse array for improving looping performance
				narray.reverse()
				var root_id = df._extract_root(mind, narray)
				if (!!root_id) {
					df._extract_subnode(mind, root_id, narray)
				} else {
					logger.error('root node can not be found')
				}
			},

			_extract_root: function (mind, node_array) {
				var df = jm.format.node_array
				var i = node_array.length
				while (i--) {
					if ('isroot' in node_array[i] && node_array[i].isroot) {
						var root_json = node_array[i]
						var data = df._extract_data(root_json)
						mind.set_root(root_json.id, root_json.noteName, data)
						node_array.splice(i, 1)
						return root_json.id
					}
				}
				return null
			},

			_extract_subnode: function (mind, parentid, node_array) {
				var df = jm.format.node_array
				var i = node_array.length
				var node_json = null
				var data = null
				var extract_count = 0
				while (i--) {
					node_json = node_array[i]
					if (node_json.parentid == parentid) {
						data = df._extract_data(node_json)
						var d = null
						var node_direction = node_json.direction
						if (!!node_direction) {
							d = node_direction == 'left' ? jm.direction.left : jm.direction.right
						}
						mind.add_node(
							parentid,
							node_json.id,
							node_json.noteName,
							data,
							null,
							d,
							node_json.expanded
						)
						node_array.splice(i, 1)
						extract_count++
						var sub_extract_count = df._extract_subnode(mind, node_json.id, node_array)
						if (sub_extract_count > 0) {
							// reset loop index after extract subordinate node
							i = node_array.length
							extract_count += sub_extract_count
						}
					}
				}
				return extract_count
			},

			_extract_data: function (node_json) {
				var data = {}
				for (var k in node_json) {
					if (
						k == 'id' ||
						k == 'noteName' ||
						k == 'parentid' ||
						k == 'isroot' ||
						k == 'direction' ||
						k == 'expanded'
					) {
						continue
					}
					data[k] = node_json[k]
				}
				return data
			},

			_array: function (mind, node_array) {
				var df = jm.format.node_array
				df._array_node(mind.root, node_array)
			},

			_array_node: function (node, node_array) {
				var df = jm.format.node_array
				if (!(node instanceof jm.node)) {
					return
				}
				var o = {
					id: node.id,
					noteName: node.noteName,
					expanded: node.expanded
				}
				if (!!node.parent) {
					o.parentid = node.parent.id
				}
				if (node.isroot) {
					o.isroot = true
				}
				if (!!node.parent && node.parent.isroot) {
					o.direction = node.direction == jm.direction.left ? 'left' : 'right'
				}
				if (node.data != null) {
					var node_data = node.data
					for (var k in node_data) {
						o[k] = node_data[k]
					}
				}
				node_array.push(o)
				var ci = node.children.length
				for (var i = 0; i < ci; i++) {
					df._array_node(node.children[i], node_array)
				}
			}
		},

		freemind: {
			example: {
				meta: {
					name: __name__,
					author: __author__,
					version: __version__
				},
				format: 'freemind',
				data: '<map version="1.0.1"><node ID="root" TEXT="freemind Example"/></map>'
			},
			get_mind: function (source) {
				var df = jm.format.freemind
				var mind = new jm.mind()
				mind.name = source.meta.name
				mind.author = source.meta.author
				mind.version = source.meta.version
				var xml = source.data
				var xml_doc = df._parse_xml(xml)
				var xml_root = df._find_root(xml_doc)
				df._load_node(mind, null, xml_root)
				return mind
			},

			get_data: function (mind) {
				var df = jm.format.freemind
				var json = {}
				json.meta = {
					name: mind.name,
					author: mind.author,
					version: mind.version
				}
				json.format = 'freemind'
				var xmllines = []
				xmllines.push('<map version="1.0.1">')
				df._buildmap(mind.root, xmllines)
				xmllines.push('</map>')
				json.data = xmllines.join(' ')
				return json
			},

			_parse_xml: function (xml) {
				var xml_doc = null
				if (window.DOMParser) {
					var parser = new DOMParser()
					xml_doc = parser.parseFromString(xml, 'text/xml')
				} else {
					// Internet Explorer
					xml_doc = new ActiveXObject('Microsoft.XMLDOM')
					xml_doc.async = false
					xml_doc.loadXML(xml)
				}
				return xml_doc
			},

			_find_root: function (xml_doc) {
				var nodes = xml_doc.childNodes
				var node = null
				var root = null
				var n = null
				for (var i = 0; i < nodes.length; i++) {
					n = nodes[i]
					if (n.nodeType == 1 && n.tagName == 'map') {
						node = n
						break
					}
				}
				if (!!node) {
					var ns = node.childNodes
					node = null
					for (var i = 0; i < ns.length; i++) {
						n = ns[i]
						if (n.nodeType == 1 && n.tagName == 'node') {
							node = n
							break
						}
					}
				}
				return node
			},

			_load_node: function (mind, parent_id, xml_node) {
				var df = jm.format.freemind
				var node_id = xml_node.getAttribute('ID')
				var node_noteName = xml_node.getAttribute('TEXT')
				// look for richcontent
				if (node_noteName == null) {
					var noteName_children = xml_node.childNodes
					var noteName_child = null
					for (var i = 0; i < noteName_children.length; i++) {
						noteName_child = noteName_children[i]
						//logger.debug(noteName_child.tagName);
						if (noteName_child.nodeType == 1 && noteName_child.tagName === 'richcontent') {
							node_noteName = noteName_child.textContent
							break
						}
					}
				}
				var node_data = df._load_attributes(xml_node)
				var node_expanded = 'expanded' in node_data ? node_data.expanded == 'true' : true
				delete node_data.expanded

				var node_position = xml_node.getAttribute('POSITION')
				var node_direction = null
				if (!!node_position) {
					node_direction = node_position == 'left' ? jm.direction.left : jm.direction.right
				}
				//logger.debug(node_position +':'+ node_direction);
				if (!!parent_id) {
					mind.add_node(
						parent_id,
						node_id,
						node_noteName,
						node_data,
						null,
						node_direction,
						node_expanded
					)
				} else {
					mind.set_root(node_id, node_noteName, node_data)
				}
				var children = xml_node.childNodes
				var child = null
				for (var i = 0; i < children.length; i++) {
					child = children[i]
					if (child.nodeType == 1 && child.tagName == 'node') {
						df._load_node(mind, node_id, child)
					}
				}
			},

			_load_attributes: function (xml_node) {
				var children = xml_node.childNodes
				var attr = null
				var attr_data = {}
				for (var i = 0; i < children.length; i++) {
					attr = children[i]
					if (attr.nodeType == 1 && attr.tagName === 'attribute') {
						attr_data[attr.getAttribute('NAME')] = attr.getAttribute('VALUE')
					}
				}
				return attr_data
			},

			_buildmap: function (node, xmllines) {
				var df = jm.format.freemind
				var pos = null
				if (!!node.parent && node.parent.isroot) {
					pos = node.direction === jm.direction.left ? 'left' : 'right'
				}
				xmllines.push('<node')
				xmllines.push('ID="' + node.id + '"')
				if (!!pos) {
					xmllines.push('POSITION="' + pos + '"')
				}
				xmllines.push('TEXT="' + node.noteName + '">')

				// store expanded status as an attribute
				xmllines.push('<attribute NAME="expanded" VALUE="' + node.expanded + '"/>')

				// for attributes
				var node_data = node.data
				if (node_data != null) {
					for (var k in node_data) {
						xmllines.push('<attribute NAME="' + k + '" VALUE="' + node_data[k] + '"/>')
					}
				}

				// for children
				var children = node.children
				for (var i = 0; i < children.length; i++) {
					df._buildmap(children[i], xmllines)
				}

				xmllines.push('</node>')
			}
		}
	}

	// ============= utility object =============================================

	jm.util = {
		is_node: function (node) {
			return !!node && node instanceof jm.node
		},
		ajax: {
			_xhr: function () {
				var xhr = null
				if (window.XMLHttpRequest) {
					xhr = new XMLHttpRequest()
				} else {
					try {
						xhr = new ActiveXObject('Microsoft.XMLHTTP')
					} catch (e) {}
				}
				return xhr
			},
			_eurl: function (url) {
				return encodeURIComponent(url)
			},
			request: function (url, param, method, callback, fail_callback) {
				var a = jm.util.ajax
				var p = null
				var tmp_param = []
				for (var k in param) {
					tmp_param.push(a._eurl(k) + '=' + a._eurl(param[k]))
				}
				if (tmp_param.length > 0) {
					p = tmp_param.join('&')
				}
				var xhr = a._xhr()
				if (!xhr) {
					return
				}
				xhr.onreadystatechange = function () {
					if (xhr.readyState == 4) {
						if (xhr.status == 200 || xhr.status == 0) {
							if (typeof callback === 'function') {
								var data = jm.util.json.string2json(xhr.responseText)
								if (data != null) {
									callback(data)
								} else {
									callback(xhr.responseText)
								}
							}
						} else {
							if (typeof fail_callback === 'function') {
								fail_callback(xhr)
							} else {
								logger.error('xhr request failed.', xhr)
							}
						}
					}
				}
				method = method || 'GET'
				xhr.open(method, url, true)
				xhr.setRequestHeader('If-Modified-Since', '0')
				if (method == 'POST') {
					xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8')
					xhr.send(p)
				} else {
					xhr.send()
				}
			},
			get: function (url, callback) {
				return jm.util.ajax.request(url, {}, 'GET', callback)
			},
			post: function (url, param, callback) {
				return jm.util.ajax.request(url, param, 'POST', callback)
			}
		},

		dom: {
			//target,eventType,handler
			add_event: function (t, e, h) {
				if (!!t.addEventListener) {
					t.addEventListener(e, h, false)
				} else {
					t.attachEvent('on' + e, h)
				}
			}
		},

		file: {
			read: function (file_data, fn_callback) {
				var reader = new FileReader()
				reader.onload = function () {
					if (typeof fn_callback === 'function') {
						fn_callback(this.result, file_data.name)
					}
				}
				reader.readAsText(file_data)
			},

			save: function (file_data, type, name) {
				var blob
				if (typeof $w.Blob === 'function') {
					blob = new Blob([file_data], { type: type })
				} else {
					var BlobBuilder =
						$w.BlobBuilder || $w.MozBlobBuilder || $w.WebKitBlobBuilder || $w.MSBlobBuilder
					var bb = new BlobBuilder()
					bb.append(file_data)
					blob = bb.getBlob(type)
				}
				if (navigator.msSaveBlob) {
					navigator.msSaveBlob(blob, name)
				} else {
					var URL = $w.URL || $w.webkitURL
					var bloburl = URL.createObjectURL(blob)
					var anchor = $c('a')
					if ('download' in anchor) {
						anchor.style.visibility = 'hidden'
						anchor.href = bloburl
						anchor.download = name
						$d.body.appendChild(anchor)
						var evt = $d.createEvent('MouseEvents')
						evt.initEvent('click', true, true)
						anchor.dispatchEvent(evt)
						$d.body.removeChild(anchor)
					} else {
						location.href = bloburl
					}
				}
			}
		},

		json: {
			json2string: function (json) {
				if (!!JSON) {
					try {
						var json_str = JSON.stringify(json)
						return json_str
					} catch (e) {
						logger.warn(e)
						logger.warn('can not convert to string')
						return null
					}
				}
			},
			string2json: function (json_str) {
				if (!!JSON) {
					try {
						var json = JSON.parse(json_str)
						return json
					} catch (e) {
						logger.warn(e)
						logger.warn('can not parse to json')
						return null
					}
				}
			},
			merge: function (b, a) {
				for (var o in a) {
					if (o in b) {
						if (
							typeof b[o] === 'object' &&
							Object.prototype.toString.call(b[o]).toLowerCase() == '[object object]' &&
							!b[o].length
						) {
							jm.util.json.merge(b[o], a[o])
						} else {
							b[o] = a[o]
						}
					} else {
						b[o] = a[o]
					}
				}
				return b
			}
		},

		uuid: {
			newid: function () {
				return (new Date().getTime().toString(16) + Math.random().toString(16).substr(2)).substr(
					2,
					16
				)
			}
		},

		text: {
			is_empty: function (s) {
				if (!s) {
					return true
				}
				return s.replace(/\s*/, '').length == 0
			}
		}
	}

	jm.prototype = {
		init: function () {
			if (this.inited) {
				return
			}
			this.inited = true

			var opts = this.options

			var opts_layout = {
				mode: opts.mode,
				hspace: opts.layout.hspace,
				vspace: opts.layout.vspace,
				pspace: opts.layout.pspace
			}
			var opts_view = {
				container: opts.container,
				support_html: opts.support_html,
				engine: opts.view.engine,
				hmargin: opts.view.hmargin,
				vmargin: opts.view.vmargin,
				line_width: opts.view.line_width,
				line_color: opts.view.line_color
			}
			// create instance of function provider
			this.data = new jm.data_provider(this)
			this.layout = new jm.layout_provider(this, opts_layout)
			this.view = new jm.view_provider(this, opts_view)
			this.shortcut = new jm.shortcut_provider(this, opts.shortcut)

			this.data.init()
			this.layout.init()
			this.view.init()
			this.shortcut.init()

			this._event_bind()

			jm.init_plugins(this)
		},

		enable_edit: function () {
			this.options.editable = true
		},

		disable_edit: function () {
			this.options.editable = false
		},

		// call enable_event_handle('dblclick')
		// options are 'mousedown', 'click', 'dblclick'
		enable_event_handle: function (event_handle) {
			this.options.default_event_handle['enable_' + event_handle + '_handle'] = true
		},

		// call disable_event_handle('dblclick')
		// options are 'mousedown', 'click', 'dblclick'
		disable_event_handle: function (event_handle) {
			this.options.default_event_handle['enable_' + event_handle + '_handle'] = false
		},

		get_editable: function () {
			return this.options.editable
		},

		set_theme: function (theme) {
			var theme_old = this.options.theme
			this.options.theme = !!theme ? theme : null
			if (theme_old != this.options.theme) {
				this.view.reset_theme()
				this.view.reset_custom_style()
			}
		},
		_event_bind: function () {
			this.view.add_event(this, 'mousedown', this.mousedown_handle)
			this.view.add_event(this, 'click', this.click_handle)
			this.view.add_event(this, 'dblclick', this.dblclick_handle)
		},

		mousedown_handle: function (e) {
			if (!this.options.default_event_handle['enable_mousedown_handle']) {
				return
			}
			var element = e.target || event.srcElement
			var nodeid = this.view.get_binded_nodeid(element)
			if (!!nodeid) {
				if (element.tagName.toLowerCase() == 'jmnode') {
					this.select_node(nodeid)
				}
			} else {
				this.select_clear()
			}
		},

		click_handle: function (e) {
			if (!this.options.default_event_handle['enable_click_handle']) {
				return
			}
			var element = e.target || event.srcElement
			var isexpander = this.view.is_expander(element)
			if (isexpander) {
				var nodeid = this.view.get_binded_nodeid(element)
				if (!!nodeid) {
					this.toggle_node(nodeid)
				}
			}
		},

		dblclick_handle: function (e) {
			if (!this.options.default_event_handle['enable_dblclick_handle']) {
				return
			}
			if (this.get_editable()) {
				var element = e.target || event.srcElement
				var nodeid = this.view.get_binded_nodeid(element)
				if (!!nodeid) {
					this.begin_edit(nodeid)
				}
			}
		},

		begin_edit: function (node) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return false
				} else {
					return this.begin_edit(the_node)
				}
			}
			if (this.get_editable()) {
				this.view.edit_node_begin(node)
			} else {
				logger.error('fail, this mind map is not editable.')
				return
			}
		},

		end_edit: function () {
			this.view.edit_node_end()
		},

		toggle_node: function (node) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return
				} else {
					return this.toggle_node(the_node)
				}
			}
			if (node.isroot) {
				return
			}
			this.view.save_location(node)
			this.layout.toggle_node(node)
			this.view.relayout()
			this.view.restore_location(node)
		},

		expand_node: function (node) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return
				} else {
					return this.expand_node(the_node)
				}
			}
			if (node.isroot) {
				return
			}
			this.view.save_location(node)
			this.layout.expand_node(node)
			this.view.relayout()
			this.view.restore_location(node)
		},

		collapse_node: function (node) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return
				} else {
					return this.collapse_node(the_node)
				}
			}
			if (node.isroot) {
				return
			}
			this.view.save_location(node)
			this.layout.collapse_node(node)
			this.view.relayout()
			this.view.restore_location(node)
		},

		expand_all: function () {
			this.layout.expand_all()
			this.view.relayout()
		},

		collapse_all: function () {
			this.layout.collapse_all()
			this.view.relayout()
		},

		expand_to_depth: function (depth) {
			this.layout.expand_to_depth(depth)
			this.view.relayout()
		},

		_reset: function () {
			this.view.reset()
			this.layout.reset()
			this.data.reset()
		},

		_show: function (mind) {
			var m = mind || jm.format.node_array.example

			this.mind = this.data.load(m)
			if (!this.mind) {
				logger.error('data.load error')
				return
			} else {
				logger.debug('data.load ok')
			}

			this.view.load()
			logger.debug('view.load ok')

			this.layout.layout()
			logger.debug('layout.layout ok')

			this.view.show(true)
			logger.debug('view.show ok')

			this.invoke_event_handle(jm.event_type.show, { data: [mind] })
		},

		show: function (mind) {
			this._reset()
			this._show(mind)
		},

		get_meta: function () {
			return {
				name: this.mind.name,
				author: this.mind.author,
				version: this.mind.version
			}
		},

		get_data: function (data_format) {
			var df = data_format || 'node_tree'
			return this.data.get_data(df)
		},

		get_root: function () {
			return this.mind.root
		},

		get_node: function (nodeid) {
			return this.mind.get_node(nodeid)
		},

		add_node: function (parent_node, nodeid, noteName, data) {
			if (this.get_editable()) {
				var node = this.mind.add_node(parent_node, nodeid, noteName, data)
				if (!!node) {
					this.view.add_node(node)
					this.layout.layout()
					this.view.show(false)
					this.view.reset_node_custom_style(node)
					this.expand_node(parent_node)
					this.invoke_event_handle(jm.event_type.edit, {
						evt: 'add_node',
						data: [parent_node.id, nodeid, noteName, data],
						node: nodeid
					})
				}
				return node
			} else {
				logger.error('fail, this mind map is not editable')
				return null
			}
		},

		insert_node_before: function (node_before, nodeid, noteName, data) {
			if (this.get_editable()) {
				var beforeid = jm.util.is_node(node_before) ? node_before.id : node_before
				var node = this.mind.insert_node_before(node_before, nodeid, noteName, data)
				if (!!node) {
					this.view.add_node(node)
					this.layout.layout()
					this.view.show(false)
					this.invoke_event_handle(jm.event_type.edit, {
						evt: 'insert_node_before',
						data: [beforeid, nodeid, noteName, data],
						node: nodeid
					})
				}
				return node
			} else {
				logger.error('fail, this mind map is not editable')
				return null
			}
		},

		insert_node_after: function (node_after, nodeid, noteName, data) {
			if (this.get_editable()) {
				var afterid = jm.util.is_node(node_after) ? node_after.id : node_after
				var node = this.mind.insert_node_after(node_after, nodeid, noteName, data)
				if (!!node) {
					this.view.add_node(node)
					this.layout.layout()
					this.view.show(false)
					this.invoke_event_handle(jm.event_type.edit, {
						evt: 'insert_node_after',
						data: [afterid, nodeid, noteName, data],
						node: nodeid
					})
				}
				return node
			} else {
				logger.error('fail, this mind map is not editable')
				return null
			}
		},

		remove_node: function (node) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return false
				} else {
					return this.remove_node(the_node)
				}
			}
			if (this.get_editable()) {
				if (node.isroot) {
					logger.error('fail, can not remove root node')
					return false
				}
				var nodeid = node.id
				var parentid = node.parent.id
				var parent_node = this.get_node(parentid)
				this.view.save_location(parent_node)
				this.view.remove_node(node)
				this.mind.remove_node(node)
				this.layout.layout()
				this.view.show(false)
				this.view.restore_location(parent_node)
				this.invoke_event_handle(jm.event_type.edit, {
					evt: 'remove_node',
					data: [nodeid],
					node: parentid
				})
				return true
			} else {
				logger.error('fail, this mind map is not editable')
				return false
			}
		},

		update_node: function (nodeid, noteName, fraction, node) {
			// !changed
			if (this.get_editable()) {
				if (jm.util.text.is_empty(noteName)) {
					logger.warn('fail, noteName can not be empty')
					return
				}
				var node = this.get_node(node.data.idLevel)
				if (!!node) {
					// if (node.noteName === noteName) {
					if (node.data['fraction'] === fraction) {
						logger.info('nothing changed')
						this.view.update_node(node)
						return
					}
					node.noteName = noteName
					node.data['fraction'] = Number(fraction)
					this.view.update_node(node)
					this.layout.layout()
					this.view.show(false)
					this.invoke_event_handle(jm.event_type.edit, {
						evt: 'update_node',
						data: [nodeid, noteName],
						node: nodeid
					})
				}
			} else {
				logger.error('fail, this mind map is not editable')
				return
			}
		},

		move_node: function (nodeid, beforeid, parentid, direction) {
			if (this.get_editable()) {
				var node = this.mind.move_node(nodeid, beforeid, parentid, direction)
				if (!!node) {
					this.view.update_node(node)
					this.layout.layout()
					this.view.show(false)
					this.invoke_event_handle(jm.event_type.edit, {
						evt: 'move_node',
						data: [nodeid, beforeid, parentid, direction],
						node: nodeid
					})
				}
			} else {
				logger.error('fail, this mind map is not editable')
				return
			}
		},

		select_node: function (node) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return
				} else {
					return this.select_node(the_node)
				}
			}
			if (!this.layout.is_visible(node)) {
				return
			}
			this.mind.selected = node
			this.view.select_node(node)
			this.invoke_event_handle(jm.event_type.select, {
				evt: 'select_node',
				data: [],
				node: node.id
			})
		},

		get_selected_node: function () {
			if (!!this.mind) {
				return this.mind.selected
			} else {
				return null
			}
		},

		select_clear: function () {
			if (!!this.mind) {
				this.mind.selected = null
				this.view.select_clear()
			}
		},

		is_node_visible: function (node) {
			return this.layout.is_visible(node)
		},

		find_node_before: function (node) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return
				} else {
					return this.find_node_before(the_node)
				}
			}
			if (node.isroot) {
				return null
			}
			var n = null
			if (node.parent.isroot) {
				var c = node.parent.children
				var prev = null
				var ni = null
				for (var i = 0; i < c.length; i++) {
					ni = c[i]
					if (node.direction === ni.direction) {
						if (node.id === ni.id) {
							n = prev
						}
						prev = ni
					}
				}
			} else {
				n = this.mind.get_node_before(node)
			}
			return n
		},

		find_node_after: function (node) {
			if (!jm.util.is_node(node)) {
				var the_node = this.get_node(node)
				if (!the_node) {
					logger.error('the node[id=' + node + '] can not be found.')
					return
				} else {
					return this.find_node_after(the_node)
				}
			}
			if (node.isroot) {
				return null
			}
			var n = null
			if (node.parent.isroot) {
				var c = node.parent.children
				var getthis = false
				var ni = null
				for (var i = 0; i < c.length; i++) {
					ni = c[i]
					if (node.direction === ni.direction) {
						if (getthis) {
							n = ni
							break
						}
						if (node.id === ni.id) {
							getthis = true
						}
					}
				}
			} else {
				n = this.mind.get_node_after(node)
			}
			return n
		},

		set_node_color: function (nodeid, bgcolor, fgcolor) {
			if (this.get_editable()) {
				var node = this.mind.get_node(nodeid)
				if (!!node) {
					if (!!bgcolor) {
						node.data['background-color'] = bgcolor
					}
					if (!!fgcolor) {
						node.data['foreground-color'] = fgcolor
					}
					this.view.reset_node_custom_style(node)
				}
			} else {
				logger.error('fail, this mind map is not editable')
				return null
			}
		},

		set_node_font_style: function (nodeid, size, weight, style) {
			if (this.get_editable()) {
				var node = this.mind.get_node(nodeid)
				if (!!node) {
					if (!!size) {
						node.data['font-size'] = size
					}
					if (!!weight) {
						node.data['font-weight'] = weight
					}
					if (!!style) {
						node.data['font-style'] = style
					}
					this.view.reset_node_custom_style(node)
					this.view.update_node(node)
					this.layout.layout()
					this.view.show(false)
				}
			} else {
				logger.error('fail, this mind map is not editable')
				return null
			}
		},

		set_node_background_image: function (nodeid, image, width, height, rotation) {
			if (this.get_editable()) {
				var node = this.mind.get_node(nodeid)
				if (!!node) {
					if (!!image) {
						node.data['background-image'] = image
					}
					if (!!width) {
						node.data['width'] = width
					}
					if (!!height) {
						node.data['height'] = height
					}
					if (!!rotation) {
						node.data['background-rotation'] = rotation
					}
					this.view.reset_node_custom_style(node)
					this.view.update_node(node)
					this.layout.layout()
					this.view.show(false)
				}
			} else {
				logger.error('fail, this mind map is not editable')
				return null
			}
		},

		set_node_background_rotation: function (nodeid, rotation) {
			if (this.get_editable()) {
				var node = this.mind.get_node(nodeid)
				if (!!node) {
					if (!node.data['background-image']) {
						logger.error('fail, only can change rotation angle of node with background image')
						return null
					}
					node.data['background-rotation'] = rotation
					this.view.reset_node_custom_style(node)
					this.view.update_node(node)
					this.layout.layout()
					this.view.show(false)
				}
			} else {
				logger.error('fail, this mind map is not editable')
				return null
			}
		},

		resize: function () {
			this.view.resize()
		},

		// callback(type ,data)
		add_event_listener: function (callback) {
			if (typeof callback === 'function') {
				this.event_handles.push(callback)
			}
		},

		clear_event_listener: function () {
			this.event_handles = []
		},

		invoke_event_handle: function (type, data) {
			var j = this
			$w.setTimeout(function () {
				j._invoke_event_handle(type, data)
			}, 0)
		},

		_invoke_event_handle: function (type, data) {
			var l = this.event_handles.length
			for (var i = 0; i < l; i++) {
				this.event_handles[i](type, data)
			}
		}
	}

	// ============= data provider =============================================

	jm.data_provider = function (jm) {
		this.jm = jm
	}

	jm.data_provider.prototype = {
		init: function () {
			logger.debug('data.init')
		},

		reset: function () {
			logger.debug('data.reset')
		},

		load: function (mind_data) {
			var df = null
			var mind = null
			if (typeof mind_data === 'object') {
				if (!!mind_data.format) {
					df = mind_data.format
				} else {
					df = 'node_tree'
				}
			} else {
				df = 'freemind'
			}

			if (df == 'node_array') {
				mind = jm.format.node_array.get_mind(mind_data)
			} else if (df == 'node_tree') {
				mind = jm.format.node_tree.get_mind(mind_data)
			} else if (df == 'freemind') {
				mind = jm.format.freemind.get_mind(mind_data)
			} else {
				logger.warn('unsupported format')
			}
			return mind
		},

		get_data: function (data_format) {
			var data = null
			if (data_format == 'node_array') {
				data = jm.format.node_array.get_data(this.jm.mind)
			} else if (data_format == 'node_tree') {
				data = jm.format.node_tree.get_data(this.jm.mind)
			} else if (data_format == 'freemind') {
				data = jm.format.freemind.get_data(this.jm.mind)
			} else {
				logger.error('unsupported ' + data_format + ' format')
			}
			return data
		}
	}

	// ============= layout provider ===========================================

	jm.layout_provider = function (jm, options) {
		this.opts = options
		this.jm = jm
		this.isside = this.opts.mode == 'side'
		this.bounds = null

		this.cache_valid = false
	}

	jm.layout_provider.prototype = {
		init: function () {
			logger.debug('layout.init')
		},
		reset: function () {
			logger.debug('layout.reset')
			this.bounds = { n: 0, s: 0, w: 0, e: 0 }
		},
		layout: function () {
			logger.debug('layout.layout')
			this.layout_direction()
			this.layout_offset()
		},

		layout_direction: function () {
			this._layout_direction_root()
		},

		_layout_direction_root: function () {
			var node = this.jm.mind.root
			// logger.debug(node);
			var layout_data = null
			if ('layout' in node._data) {
				layout_data = node._data.layout
			} else {
				layout_data = {}
				node._data.layout = layout_data
			}
			var children = node.children
			var children_count = children.length
			layout_data.direction = jm.direction.center
			layout_data.side_index = 0
			if (this.isside) {
				var i = children_count
				while (i--) {
					this._layout_direction_side(children[i], jm.direction.right, i)
				}
			} else {
				var i = children_count
				var subnode = null
				while (i--) {
					subnode = children[i]
					if (subnode.direction == jm.direction.left) {
						this._layout_direction_side(subnode, jm.direction.left, i)
					} else {
						this._layout_direction_side(subnode, jm.direction.right, i)
					}
				}
				/*
                var boundary = Math.ceil(children_count/2);
                var i = children_count;
                while(i--){
                    if(i>=boundary){
                        this._layout_direction_side(children[i],jm.direction.left, children_count-i-1);
                    }else{
                        this._layout_direction_side(children[i],jm.direction.right, i);
                    }
                }*/
			}
		},

		_layout_direction_side: function (node, direction, side_index) {
			var layout_data = null
			if ('layout' in node._data) {
				layout_data = node._data.layout
			} else {
				layout_data = {}
				node._data.layout = layout_data
			}
			var children = node.children
			var children_count = children.length

			layout_data.direction = direction
			layout_data.side_index = side_index
			var i = children_count
			while (i--) {
				this._layout_direction_side(children[i], direction, i)
			}
		},

		layout_offset: function () {
			var node = this.jm.mind.root
			var layout_data = node._data.layout
			layout_data.offset_x = 0
			layout_data.offset_y = 0
			layout_data.outer_height = 0
			var children = node.children
			var i = children.length
			var left_nodes = []
			var right_nodes = []
			var subnode = null
			while (i--) {
				subnode = children[i]
				if (subnode._data.layout.direction == jm.direction.right) {
					right_nodes.unshift(subnode)
				} else {
					left_nodes.unshift(subnode)
				}
			}
			layout_data.left_nodes = left_nodes
			layout_data.right_nodes = right_nodes
			layout_data.outer_height_left = this._layout_offset_subnodes(left_nodes)
			layout_data.outer_height_right = this._layout_offset_subnodes(right_nodes)
			this.bounds.e = node._data.view.width / 2
			this.bounds.w = 0 - this.bounds.e
			//logger.debug(this.bounds.w);
			this.bounds.n = 0
			this.bounds.s = Math.max(layout_data.outer_height_left, layout_data.outer_height_right)
		},

		// layout both the x and y axis
		_layout_offset_subnodes: function (nodes) {
			var total_height = 0
			var nodes_count = nodes.length
			var i = nodes_count
			var node = null
			var node_outer_height = 0
			var layout_data = null
			var base_y = 0
			var pd = null // parent._data
			while (i--) {
				node = nodes[i]
				layout_data = node._data.layout
				if (pd == null) {
					pd = node.parent._data
				}

				node_outer_height = this._layout_offset_subnodes(node.children)
				if (!node.expanded) {
					node_outer_height = 0
					this.set_visible(node.children, false)
				}
				node_outer_height = Math.max(node._data.view.height, node_outer_height)

				layout_data.outer_height = node_outer_height
				layout_data.offset_y = base_y - node_outer_height / 2
				layout_data.offset_x =
					this.opts.hspace * layout_data.direction +
					(pd.view.width * (pd.layout.direction + layout_data.direction)) / 2
				if (!node.parent.isroot) {
					layout_data.offset_x += this.opts.pspace * layout_data.direction
				}

				base_y = base_y - node_outer_height - this.opts.vspace
				total_height += node_outer_height
			}
			if (nodes_count > 1) {
				total_height += this.opts.vspace * (nodes_count - 1)
			}
			i = nodes_count
			var middle_height = total_height / 2
			while (i--) {
				node = nodes[i]
				node._data.layout.offset_y += middle_height
			}
			return total_height
		},

		// layout the y axis only, for collapse/expand a node
		_layout_offset_subnodes_height: function (nodes) {
			var total_height = 0
			var nodes_count = nodes.length
			var i = nodes_count
			var node = null
			var node_outer_height = 0
			var layout_data = null
			var base_y = 0
			var pd = null // parent._data
			while (i--) {
				node = nodes[i]
				layout_data = node._data.layout
				if (pd == null) {
					pd = node.parent._data
				}

				node_outer_height = this._layout_offset_subnodes_height(node.children)
				if (!node.expanded) {
					node_outer_height = 0
				}
				node_outer_height = Math.max(node._data.view.height, node_outer_height)

				layout_data.outer_height = node_outer_height
				layout_data.offset_y = base_y - node_outer_height / 2
				base_y = base_y - node_outer_height - this.opts.vspace
				total_height += node_outer_height
			}
			if (nodes_count > 1) {
				total_height += this.opts.vspace * (nodes_count - 1)
			}
			i = nodes_count
			var middle_height = total_height / 2
			while (i--) {
				node = nodes[i]
				node._data.layout.offset_y += middle_height
				//logger.debug(node.noteName);
				//logger.debug(node._data.layout.offset_y);
			}
			return total_height
		},

		get_node_offset: function (node) {
			var layout_data = node._data.layout
			var offset_cache = null
			if ('_offset_' in layout_data && this.cache_valid) {
				offset_cache = layout_data._offset_
			} else {
				offset_cache = { x: -1, y: -1 }
				layout_data._offset_ = offset_cache
			}
			if (offset_cache.x == -1 || offset_cache.y == -1) {
				var x = layout_data.offset_x
				var y = layout_data.offset_y
				if (!node.isroot) {
					var offset_p = this.get_node_offset(node.parent)
					x += offset_p.x
					y += offset_p.y
				}
				offset_cache.x = x
				offset_cache.y = y
			}
			return offset_cache
		},

		get_node_point: function (node) {
			var view_data = node._data.view
			var offset_p = this.get_node_offset(node)
			//logger.debug(offset_p);
			var p = {}
			p.x = offset_p.x + (view_data.width * (node._data.layout.direction - 1)) / 2
			p.y = offset_p.y - view_data.height / 2
			//logger.debug(p);
			return p
		},

		get_node_point_in: function (node) {
			var p = this.get_node_offset(node)
			return p
		},

		get_node_point_out: function (node) {
			var layout_data = node._data.layout
			var pout_cache = null
			if ('_pout_' in layout_data && this.cache_valid) {
				pout_cache = layout_data._pout_
			} else {
				pout_cache = { x: -1, y: -1 }
				layout_data._pout_ = pout_cache
			}
			if (pout_cache.x == -1 || pout_cache.y == -1) {
				if (node.isroot) {
					pout_cache.x = 0
					pout_cache.y = 0
				} else {
					var view_data = node._data.view
					var offset_p = this.get_node_offset(node)
					pout_cache.x =
						offset_p.x + (view_data.width + this.opts.pspace) * node._data.layout.direction
					pout_cache.y = offset_p.y
					//logger.debug('pout');
					//logger.debug(pout_cache);
				}
			}
			return pout_cache
		},

		get_expander_point: function (node) {
			var p = this.get_node_point_out(node)
			var ex_p = {}
			if (node._data.layout.direction == jm.direction.right) {
				ex_p.x = p.x - this.opts.pspace
			} else {
				ex_p.x = p.x
			}
			ex_p.y = p.y - Math.ceil(this.opts.pspace / 2)
			return ex_p
		},

		get_min_size: function () {
			var nodes = this.jm.mind.nodes
			var node = null
			var pout = null
			for (var nodeid in nodes) {
				node = nodes[nodeid]
				pout = this.get_node_point_out(node)
				if (pout.x > this.bounds.e) {
					this.bounds.e = pout.x
				}
				if (pout.x < this.bounds.w) {
					this.bounds.w = pout.x
				}
			}
			return {
				w: this.bounds.e - this.bounds.w,
				h: this.bounds.s - this.bounds.n
			}
		},

		toggle_node: function (node) {
			if (node.isroot) {
				return
			}
			if (node.expanded) {
				this.collapse_node(node)
			} else {
				this.expand_node(node)
			}
		},

		expand_node: function (node) {
			node.expanded = true
			this.part_layout(node)
			this.set_visible(node.children, true)
			this.jm.invoke_event_handle(jm.event_type.show, {
				evt: 'expand_node',
				data: [],
				node: node.id
			})
		},

		collapse_node: function (node) {
			node.expanded = false
			this.part_layout(node)
			this.set_visible(node.children, false)
			this.jm.invoke_event_handle(jm.event_type.show, {
				evt: 'collapse_node',
				data: [],
				node: node.id
			})
		},

		expand_all: function () {
			var nodes = this.jm.mind.nodes
			var c = 0
			var node
			for (var nodeid in nodes) {
				node = nodes[nodeid]
				if (!node.expanded) {
					node.expanded = true
					c++
				}
			}
			if (c > 0) {
				var root = this.jm.mind.root
				this.part_layout(root)
				this.set_visible(root.children, true)
			}
		},

		collapse_all: function () {
			var nodes = this.jm.mind.nodes
			var c = 0
			var node
			for (var nodeid in nodes) {
				node = nodes[nodeid]
				if (node.expanded && !node.isroot) {
					node.expanded = false
					c++
				}
			}
			if (c > 0) {
				var root = this.jm.mind.root
				this.part_layout(root)
				this.set_visible(root.children, true)
			}
		},

		expand_to_depth: function (target_depth, curr_nodes, curr_depth) {
			if (target_depth < 1) {
				return
			}
			var nodes = curr_nodes || this.jm.mind.root.children
			var depth = curr_depth || 1
			var i = nodes.length
			var node = null
			while (i--) {
				node = nodes[i]
				if (depth < target_depth) {
					if (!node.expanded) {
						this.expand_node(node)
					}
					this.expand_to_depth(target_depth, node.children, depth + 1)
				}
				if (depth == target_depth) {
					if (node.expanded) {
						this.collapse_node(node)
					}
				}
			}
		},

		part_layout: function (node) {
			var root = this.jm.mind.root
			if (!!root) {
				var root_layout_data = root._data.layout
				if (node.isroot) {
					root_layout_data.outer_height_right = this._layout_offset_subnodes_height(
						root_layout_data.right_nodes
					)
					root_layout_data.outer_height_left = this._layout_offset_subnodes_height(
						root_layout_data.left_nodes
					)
				} else {
					if (node._data.layout.direction == jm.direction.right) {
						root_layout_data.outer_height_right = this._layout_offset_subnodes_height(
							root_layout_data.right_nodes
						)
					} else {
						root_layout_data.outer_height_left = this._layout_offset_subnodes_height(
							root_layout_data.left_nodes
						)
					}
				}
				this.bounds.s = Math.max(
					root_layout_data.outer_height_left,
					root_layout_data.outer_height_right
				)
				this.cache_valid = false
			} else {
				logger.warn('can not found root node')
			}
		},

		set_visible: function (nodes, visible) {
			var i = nodes.length
			var node = null
			var layout_data = null
			while (i--) {
				node = nodes[i]
				layout_data = node._data.layout
				if (node.expanded) {
					this.set_visible(node.children, visible)
				} else {
					this.set_visible(node.children, false)
				}
				if (!node.isroot) {
					node._data.layout.visible = visible
				}
			}
		},

		is_expand: function (node) {
			return node.expanded
		},

		is_visible: function (node) {
			var layout_data = node._data.layout
			if ('visible' in layout_data && !layout_data.visible) {
				return false
			} else {
				return true
			}
		}
	}

	jm.graph_canvas = function (view) {
		this.opts = view.opts
		this.e_canvas = $c('canvas')
		this.e_canvas.className = 'jsmind'
		this.canvas_ctx = this.e_canvas.getContext('2d')
		this.size = { w: 0, h: 0 }
	}

	jm.graph_canvas.prototype = {
		element: function () {
			return this.e_canvas
		},

		set_size: function (w, h) {
			this.size.w = w
			this.size.h = h
			this.e_canvas.width = w
			this.e_canvas.height = h
		},

		clear: function () {
			this.canvas_ctx.clearRect(0, 0, this.size.w, this.size.h)
		},

		draw_line: function (pout, pin, offset) {
			var ctx = this.canvas_ctx
			ctx.strokeStyle = this.opts.line_color
			ctx.lineWidth = this.opts.line_width
			ctx.lineCap = 'round'

			this._bezier_to(ctx, pin.x + offset.x, pin.y + offset.y, pout.x + offset.x, pout.y + offset.y)
		},

		copy_to: function (dest_canvas_ctx, callback) {
			dest_canvas_ctx.drawImage(this.e_canvas, 0, 0)
			!!callback && callback()
		},

		_bezier_to: function (ctx, x1, y1, x2, y2) {
			ctx.beginPath()
			ctx.moveTo(x1, y1)
			ctx.bezierCurveTo(x1 + ((x2 - x1) * 2) / 3, y1, x1, y2, x2, y2)
			ctx.stroke()
		},

		_line_to: function (ctx, x1, y1, x2, y2) {
			ctx.beginPath()
			ctx.moveTo(x1, y1)
			ctx.lineTo(x2, y2)
			ctx.stroke()
		}
	}

	jm.graph_svg = function (view) {
		this.view = view
		this.opts = view.opts
		this.e_svg = jm.graph_svg.c('svg')
		this.e_svg.setAttribute('class', 'jsmind')
		this.size = { w: 0, h: 0 }
		this.lines = []
	}

	jm.graph_svg.c = function (tag) {
		return $d.createElementNS('http://www.w3.org/2000/svg', tag)
	}

	jm.graph_svg.prototype = {
		element: function () {
			return this.e_svg
		},

		set_size: function (w, h) {
			this.size.w = w
			this.size.h = h
			this.e_svg.setAttribute('width', w)
			this.e_svg.setAttribute('height', h)
		},

		clear: function () {
			var len = this.lines.length
			while (len--) {
				this.e_svg.removeChild(this.lines[len])
			}
			this.lines.length = 0
		},

		draw_line: function (pout, pin, offset) {
			var line = jm.graph_svg.c('path')
			line.setAttribute('stroke', this.opts.line_color)
			line.setAttribute('stroke-width', this.opts.line_width)
			line.setAttribute('fill', 'transparent')
			this.lines.push(line)
			this.e_svg.appendChild(line)
			this._bezier_to(
				line,
				pin.x + offset.x,
				pin.y + offset.y,
				pout.x + offset.x,
				pout.y + offset.y
			)
		},

		copy_to: function (dest_canvas_ctx, callback) {
			var img = new Image()
			img.onload = function () {
				dest_canvas_ctx.drawImage(img, 0, 0)
				!!callback && callback()
			}
			img.src =
				'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(this.e_svg))
		},

		_bezier_to: function (path, x1, y1, x2, y2) {
			path.setAttribute(
				'd',
				'M' +
					x1 +
					' ' +
					y1 +
					' C ' +
					(x1 + ((x2 - x1) * 2) / 3) +
					' ' +
					y1 +
					', ' +
					x1 +
					' ' +
					y2 +
					', ' +
					x2 +
					' ' +
					y2
			)
		},

		_line_to: function (path, x1, y1, x2, y2) {
			path.setAttribute('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
		}
	}

	// view provider
	jm.view_provider = function (jm, options) {
		this.opts = options
		this.jm = jm
		this.layout = jm.layout

		this.container = null
		this.e_panel = null
		this.e_nodes = null

		this.size = { w: 0, h: 0 }

		this.selected_node = null
		this.editing_node = null

		this.graph = null
	}

	jm.view_provider.prototype = {
		init: function () {
			logger.debug('view.init')

			this.container = $i(this.opts.container) ? this.opts.container : $g(this.opts.container)
			if (!this.container) {
				logger.error('the options.view.container was not be found in dom')
				return
			}
			this.e_panel = $c('div')
			this.e_nodes = $c('jmnodes')
			this.e_editor = $c('input')

			this.graph =
				this.opts.engine.toLowerCase() === 'svg'
					? new jm.graph_svg(this)
					: new jm.graph_canvas(this)

			this.e_panel.className = 'jsmind-inner'
			this.e_panel.tabIndex = 1
			this.e_panel.appendChild(this.graph.element())
			this.e_panel.appendChild(this.e_nodes)

			this.e_editor.className = 'jsmind-editor'
			this.e_editor.type = 'text'

			this.actualZoom = 1
			this.zoomStep = 0.1
			this.minZoom = 0.5
			this.maxZoom = 2

			var v = this
			jm.util.dom.add_event(this.e_editor, 'keydown', function (e) {
				var evt = e || event
				if (evt.keyCode == 13) {
					v.edit_node_end()
					evt.stopPropagation()
				}
			})
			jm.util.dom.add_event(this.e_editor, 'blur', function (e) {
				v.edit_node_end()
			})

			this.container.appendChild(this.e_panel)
		},

		add_event: function (obj, event_name, event_handle) {
			jm.util.dom.add_event(this.e_nodes, event_name, function (e) {
				var evt = e || event
				event_handle.call(obj, evt)
			})
		},

		get_binded_nodeid: function (element) {
			if (element == null) {
				return null
			}
			var tagName = element.tagName.toLowerCase()
			if (tagName == 'jmnodes' || tagName == 'body' || tagName == 'html') {
				return null
			}
			if (tagName == 'jmnode' || tagName == 'jmexpander') {
				return element.getAttribute('nodeid')
			} else {
				return this.get_binded_nodeid(element.parentElement)
			}
		},

		is_expander: function (element) {
			return element.tagName.toLowerCase() == 'jmexpander'
		},

		reset: function () {
			logger.debug('view.reset')
			this.selected_node = null
			this.clear_lines()
			this.clear_nodes()
			this.reset_theme()
		},

		reset_theme: function () {
			var theme_name = this.jm.options.theme
			if (!!theme_name) {
				this.e_nodes.className = 'theme-' + theme_name
			} else {
				this.e_nodes.className = ''
			}
		},

		reset_custom_style: function () {
			var nodes = this.jm.mind.nodes
			for (var nodeid in nodes) {
				this.reset_node_custom_style(nodes[nodeid])
			}
		},

		load: function () {
			logger.debug('view.load')
			this.init_nodes()
		},

		expand_size: function () {
			var min_size = this.layout.get_min_size()
			var min_width = min_size.w + this.opts.hmargin * 2
			var min_height = min_size.h + this.opts.vmargin * 2
			var client_w = this.e_panel.clientWidth
			var client_h = this.e_panel.clientHeight
			if (client_w < min_width) {
				client_w = min_width
			}
			if (client_h < min_height) {
				client_h = min_height
			}
			this.size.w = client_w
			this.size.h = client_h
		},

		init_nodes_size: function (node) {
			var view_data = node._data.view
			view_data.width = view_data.element.clientWidth
			view_data.height = view_data.element.clientHeight
		},

		init_nodes: function () {
			var nodes = this.jm.mind.nodes
			var doc_frag = $d.createDocumentFragment()
			for (var nodeid in nodes) {
				this.create_node_element(nodes[nodeid], doc_frag)
			}
			this.e_nodes.appendChild(doc_frag)
			for (var nodeid in nodes) {
				this.init_nodes_size(nodes[nodeid])
			}
		},

		add_node: function (node) {
			this.create_node_element(node, this.e_nodes)
			this.init_nodes_size(node)
		},

		create_node_element: function (node, parent_node) {
			var view_data = null
			if ('view' in node._data) {
				view_data = node._data.view
			} else {
				view_data = {}
				node._data.view = view_data
			}

			var d = $c('jmnode')
			if (node.isroot) {
				d.className = 'root'
			} else {
				var d_e = $c('jmexpander')
				$t(d_e, '-')
				d_e.setAttribute('nodeid', node.data.idLevel) // !changed 修改demo绑定属性nodeid
				d_e.style.visibility = 'hidden'
				parent_node.appendChild(d_e)
				view_data.expander = d_e
			}
			if (!!node.noteName) {
				// !changed
        const fraction = node.data['fraction'] ?? '--'
        const totalScore = node.data['totalSystemScore'] ?? '--'
        const score = node.data['score'] ?? '--'
        let colorClass = ' default'

        if (score >= 80 && score <= 100) {
          colorClass = ' success'
        } else if (score >= 70 && score <= 79) {
          colorClass = ' warning'
        } else if (score >= 60 && score <= 69) {
          colorClass = ' abnormal'
        } else if (score <= 59) {
          colorClass = ' danger'
        }

				if (this.opts.support_html) {
          if (this.opts.container === 'box-mind') {
            if (node.direction === -1) {
              $h(
                d,
                "<span class='m-value'>" +
                  fraction +
                  "</span><span class='m-label'>" +
                  node.noteName +
                  '</span>'
              )
            } else {
              $h(
                d,
                "<span class='m-label'>" +
                  node.noteName +
                  "</span><span class='m-value'>" +
                  fraction +
                  '</span>'
              )
            }
          } else {
            const typeScoreName = this.opts.container === 'mind-box-total' ? '&emsp;&emsp;&emsp;得分' : '得分'
            const typeName = this.opts.container === 'mind-box-total' ? '占系统得分' : '权重'
            const typeValue = this.opts.container === 'mind-box-total' ? totalScore : fraction
            if (node.direction === -1) {
              $h(
                d,
                `<span class='m-value${colorClass}'>
                  <span class='m-score'>${typeScoreName}：${score}</span>
                  <span class='m-score-total'>${typeName}：${typeValue}</span>
                </span>
                <span class='m-label'>${node.noteName}</span>`
              )
            } else {
              $h(
                d,
                `<span class='m-label'>${node.noteName}</span>
                <span class='m-value${colorClass}'>
                  <span class='m-score'>${typeScoreName}：${score}</span>
                  <span class='m-score-total'>${typeName}：${typeValue}</span>
                </span>`
              )
            }
          }
				} else {
					if (node.direction === -1) {
						$t(d, fraction + ':' + node.noteName)
					} else {
						$t(d, node.noteName + ':' + fraction)
					}
				}
			}
			d.setAttribute('nodeid', node.data.idLevel) // !changed 修改demo绑定属性nodeid
			d.style.visibility = 'hidden'
			this._reset_node_custom_style(d, node.data)

			parent_node.appendChild(d)
			view_data.element = d
		},

		remove_node: function (node) {
			if (this.selected_node != null && this.selected_node.id == node.id) {
				this.selected_node = null
			}
			if (this.editing_node != null && this.editing_node.id == node.id) {
				node._data.view.element.removeChild(this.e_editor)
				this.editing_node = null
			}
			var children = node.children
			var i = children.length
			while (i--) {
				this.remove_node(children[i])
			}
			if (node._data.view) {
				var element = node._data.view.element
				var expander = node._data.view.expander
				this.e_nodes.removeChild(element)
				this.e_nodes.removeChild(expander)
				node._data.view.element = null
				node._data.view.expander = null
			}
		},

		update_node: function (node) {
			var view_data = node._data.view
			var element = view_data.element
			if (!!node.noteName) {
				// !changed
        const fraction = node.data['fraction'] ?? '--'
				if (this.opts.support_html) {
					if (node.direction === -1) {
						$h(
							element,
							"<span class='m-value'>" +
								fraction +
								"</span><span class='m-label'>" +
								node.noteName +
								'</span>'
						)
					} else {
						$h(
							element,
							"<span class='m-label'>" +
								node.noteName +
								"</span><span class='m-value'>" +
								fraction +
								'</span>'
						)
					}
				} else {
					if (node.direction === -1) {
						$t(element, fraction + ':' + node.noteName)
					} else {
						$t(element, node.noteName + ':' + fraction)
					}
				}
			}
			view_data.width = element.clientWidth
			view_data.height = element.clientHeight
		},

		select_node: function (node) {
			if (!!this.selected_node) {
				this.selected_node._data.view.element.className =
					this.selected_node._data.view.element.className.replace(/\s*selected\b/i, '')
				this.reset_node_custom_style(this.selected_node)
			}
			if (!!node) {
				this.selected_node = node
				node._data.view.element.className += ' selected'
				this.clear_node_custom_style(node)
			}
		},

		select_clear: function () {
			this.select_node(null)
		},

		get_editing_node: function () {
			return this.editing_node
		},

		is_editing: function () {
			return !!this.editing_node
		},

		edit_node_begin: function (node) {
			if (!node.noteName) {
				logger.warn("don't edit image nodes")
				return
			}
			if (this.editing_node != null) {
				this.edit_node_end()
			}
			this.editing_node = node
			var view_data = node._data.view
			var element = view_data.element
			var noteName = node.noteName
			const childElement = element.getElementsByClassName('m-value')[0]
			const childLabelNode = $c('span')
			childLabelNode.className = 'm-label'
			childLabelNode.innerHTML = noteName

			var ncs = getComputedStyle(element)

			// !changed
			this.e_editor.value = node.data['fraction']
			this.e_editor.style.width =
				childElement.clientWidth -
				parseInt(ncs.getPropertyValue('padding-left')) -
				parseInt(ncs.getPropertyValue('padding-right')) +
				'px'
			this.e_editor.style.backgroundColor = '#FFEEBC'
			this.e_editor.style.height = '100%'
			this.e_editor.style.padding = '0 10px'

			element.innerHTML = ''
			if (node.direction === -1) {
				element.appendChild(this.e_editor)
				element.appendChild(childLabelNode)
			} else {
				element.appendChild(childLabelNode)
				element.appendChild(this.e_editor)
			}
			element.style.zIndex = 5
			this.e_editor.focus()
			this.e_editor.select()
		},

		edit_node_end: function () {
			if (this.editing_node != null) {
				var node = this.editing_node
				this.editing_node = null
				var view_data = node._data.view
				var element = view_data.element
				var noteName = node.noteName
				var nodeScore = this.e_editor.value
				element.style.zIndex = 'auto'
				element.removeChild(this.e_editor)
				if (
					jm.util.text.is_empty(nodeScore) ||
					node.data['fraction'] == nodeScore ||
					Object.is(Number(nodeScore), NaN)
				) {
					// !changed
					this.e_editor.value = ''
					if (this.opts.support_html) {
            const fraction = node.data['fraction'] ?? '--'
						if (node.direction === -1) {
							$h(
								element,
								"<span class='m-value'>" +
									fraction +
									"</span><span class='m-label'>" +
									node.noteName +
									'</span>'
							)
						} else {
							$h(
								element,
								"<span class='m-label'>" +
									node.noteName +
									"</span><span class='m-value'>" +
									fraction +
									'</span>'
							)
						}
					} else {
						if (node.direction === -1) {
							$t(element, fraction + ':' + node.noteName)
						} else {
							$t(element, node.noteName + ':' + fraction)
						}
					}
				} else {
					this.jm.update_node(node.id, noteName, nodeScore, node)
				}
			}
		},

		get_view_offset: function () {
			var bounds = this.layout.bounds
			var _x = (this.size.w - bounds.e - bounds.w) / 2
			var _y = this.size.h / 2
			return { x: _x, y: _y }
		},

		resize: function () {
			this.graph.set_size(1, 1)
			this.e_nodes.style.width = '1px'
			this.e_nodes.style.height = '1px'

			this.expand_size()
			this._show()
		},

		_show: function () {
			this.graph.set_size(this.size.w, this.size.h)
			this.e_nodes.style.width = this.size.w + 'px'
			this.e_nodes.style.height = this.size.h + 'px'
			this.show_nodes()
			this.show_lines()
			//this.layout.cache_valid = true;
			this.jm.invoke_event_handle(jm.event_type.resize, { data: [] })
		},

		zoomIn: function () {
			return this.setZoom(this.actualZoom + this.zoomStep)
		},

		zoomOut: function () {
			return this.setZoom(this.actualZoom - this.zoomStep)
		},

		setZoom: function (zoom) {
			if (zoom < this.minZoom || zoom > this.maxZoom) {
				return false
			}
			this.actualZoom = zoom
			for (var i = 0; i < this.e_panel.children.length; i++) {
				this.e_panel.children[i].style.transform = 'scale(' + zoom + ')'
			}
			this.show(true)
			return true
		},

		_center_root: function () {
			// center root node
			var outer_w = this.e_panel.clientWidth
			var outer_h = this.e_panel.clientHeight
			if (this.size.w > outer_w) {
				var _offset = this.get_view_offset()
				this.e_panel.scrollLeft = _offset.x - outer_w / 2
			}
			if (this.size.h > outer_h) {
				this.e_panel.scrollTop = (this.size.h - outer_h) / 2
			}
		},

		show: function (keep_center) {
			logger.debug('view.show')
			this.expand_size()
			this._show()
			if (!!keep_center) {
				this._center_root()
			}
		},

		relayout: function () {
			this.expand_size()
			this._show()
		},

		save_location: function (node) {
			var vd = node._data.view
			vd._saved_location = {
				x: parseInt(vd.element.style.left) - this.e_panel.scrollLeft,
				y: parseInt(vd.element.style.top) - this.e_panel.scrollTop
			}
		},

		restore_location: function (node) {
			var vd = node._data.view
			this.e_panel.scrollLeft = parseInt(vd.element.style.left) - vd._saved_location.x
			this.e_panel.scrollTop = parseInt(vd.element.style.top) - vd._saved_location.y
		},

		clear_nodes: function () {
			var mind = this.jm.mind
			if (mind == null) {
				return
			}
			var nodes = mind.nodes
			var node = null
			for (var nodeid in nodes) {
				node = nodes[nodeid]
				node._data.view.element = null
				node._data.view.expander = null
			}
			this.e_nodes.innerHTML = ''
		},

		show_nodes: function () {
			var nodes = this.jm.mind.nodes
			var node = null
			var node_element = null
			var expander = null
			var p = null
			var p_expander = null
			var expander_text = '-'
			var view_data = null
			var _offset = this.get_view_offset()
			for (var nodeid in nodes) {
				node = nodes[nodeid]
				view_data = node._data.view
				node_element = view_data.element
				expander = view_data.expander
				if (!this.layout.is_visible(node)) {
					node_element.style.display = 'none'
					expander.style.display = 'none'
					continue
				}
				this.reset_node_custom_style(node)
				p = this.layout.get_node_point(node)
				view_data.abs_x = _offset.x + p.x
				view_data.abs_y = _offset.y + p.y
				node_element.style.left = _offset.x + p.x + 'px'
				node_element.style.top = _offset.y + p.y + 'px'
				node_element.style.display = ''
				node_element.style.visibility = 'visible'
				if (!node.isroot && node.children.length > 0) {
					expander_text = node.expanded ? '-' : '+'
					p_expander = this.layout.get_expander_point(node)
					expander.style.left = _offset.x + p_expander.x + 'px'
					expander.style.top = _offset.y + p_expander.y + 'px'
					expander.style.display = ''
					expander.style.visibility = 'visible'
					$t(expander, expander_text)
				}
				// hide expander while all children have been removed
				if (!node.isroot && node.children.length == 0) {
					expander.style.display = 'none'
					expander.style.visibility = 'hidden'
				}
			}
		},

		reset_node_custom_style: function (node) {
			this._reset_node_custom_style(node._data.view.element, node.data)
		},

		_reset_node_custom_style: function (node_element, node_data) {
			if ('background-color' in node_data) {
				node_element.style.backgroundColor = node_data['background-color']
			}
			if ('foreground-color' in node_data) {
				node_element.style.color = node_data['foreground-color']
			}
			if ('width' in node_data) {
				node_element.style.width = node_data['width'] + 'px'
			}
			if ('height' in node_data) {
				node_element.style.height = node_data['height'] + 'px'
			}
			if ('font-size' in node_data) {
				node_element.style.fontSize = node_data['font-size'] + 'px'
			}
			if ('font-weight' in node_data) {
				node_element.style.fontWeight = node_data['font-weight']
			}
			if ('font-style' in node_data) {
				node_element.style.fontStyle = node_data['font-style']
			}
			if ('background-image' in node_data) {
				var backgroundImage = node_data['background-image']
				if (backgroundImage.startsWith('data') && node_data['width'] && node_data['height']) {
					var img = new Image()

					img.onload = function () {
						var c = $c('canvas')
						c.width = node_element.clientWidth
						c.height = node_element.clientHeight
						var img = this
						if (c.getContext) {
							var ctx = c.getContext('2d')
							ctx.drawImage(img, 2, 2, node_element.clientWidth, node_element.clientHeight)
							var scaledImageData = c.toDataURL()
							node_element.style.backgroundImage = 'url(' + scaledImageData + ')'
						}
					}
					img.src = backgroundImage
				} else {
					node_element.style.backgroundImage = 'url(' + backgroundImage + ')'
				}
				node_element.style.backgroundSize = '99%'

				if ('background-rotation' in node_data) {
					node_element.style.transform = 'rotate(' + node_data['background-rotation'] + 'deg)'
				}
			}
		},

		clear_node_custom_style: function (node) {
			var node_element = node._data.view.element
			node_element.style.backgroundColor = ''
			node_element.style.color = ''
		},

		clear_lines: function () {
			this.graph.clear()
		},

		show_lines: function () {
			this.clear_lines()
			var nodes = this.jm.mind.nodes
			var node = null
			var pin = null
			var pout = null
			var _offset = this.get_view_offset()
			for (var nodeid in nodes) {
				node = nodes[nodeid]
				if (!!node.isroot) {
					continue
				}
				if ('visible' in node._data.layout && !node._data.layout.visible) {
					continue
				}
				pin = this.layout.get_node_point_in(node)
				pout = this.layout.get_node_point_out(node.parent)
				this.graph.draw_line(pout, pin, _offset)
			}
		}
	}

	// shortcut provider
	jm.shortcut_provider = function (jm, options) {
		this.jm = jm
		this.opts = options
		this.mapping = options.mapping
		this.handles = options.handles
		this._newid = null
		this._mapping = {}
	}

	jm.shortcut_provider.prototype = {
		init: function () {
			jm.util.dom.add_event(this.jm.view.e_panel, 'keydown', this.handler.bind(this))

			this.handles['addchild'] = this.handle_addchild
			this.handles['addbrother'] = this.handle_addbrother
			this.handles['editnode'] = this.handle_editnode
			this.handles['delnode'] = this.handle_delnode
			this.handles['toggle'] = this.handle_toggle
			this.handles['up'] = this.handle_up
			this.handles['down'] = this.handle_down
			this.handles['left'] = this.handle_left
			this.handles['right'] = this.handle_right

			for (var handle in this.mapping) {
				if (!!this.mapping[handle] && handle in this.handles) {
					this._mapping[this.mapping[handle]] = this.handles[handle]
				}
			}

			if (typeof this.opts.id_generator === 'function') {
				this._newid = this.opts.id_generator
			} else {
				this._newid = jm.util.uuid.newid
			}
		},

		enable_shortcut: function () {
			this.opts.enable = true
		},

		disable_shortcut: function () {
			this.opts.enable = false
		},

		handler: function (e) {
			if (e.which == 9) {
				e.preventDefault()
			} //prevent tab to change focus in browser
			if (this.jm.view.is_editing()) {
				return
			}
			var evt = e || event
			if (!this.opts.enable) {
				return true
			}
			var kc =
				evt.keyCode +
				(evt.metaKey << 13) +
				(evt.ctrlKey << 12) +
				(evt.altKey << 11) +
				(evt.shiftKey << 10)
			if (kc in this._mapping) {
				this._mapping[kc].call(this, this.jm, e)
			}
		},

		handle_addchild: function (_jm, e) {
			var selected_node = _jm.get_selected_node()
			if (!!selected_node) {
				var nodeid = this._newid()
				var node = _jm.add_node(selected_node, nodeid, 'New Node')
				if (!!node) {
					_jm.select_node(nodeid)
					_jm.begin_edit(nodeid)
				}
			}
		},
		handle_addbrother: function (_jm, e) {
			var selected_node = _jm.get_selected_node()
			if (!!selected_node && !selected_node.isroot) {
				var nodeid = this._newid()
				var node = _jm.insert_node_after(selected_node, nodeid, 'New Node')
				if (!!node) {
					_jm.select_node(nodeid)
					_jm.begin_edit(nodeid)
				}
			}
		},
		handle_editnode: function (_jm, e) {
			var selected_node = _jm.get_selected_node()
			if (!!selected_node) {
				_jm.begin_edit(selected_node)
			}
		},
		handle_delnode: function (_jm, e) {
			var selected_node = _jm.get_selected_node()
			if (!!selected_node && !selected_node.isroot) {
				_jm.select_node(selected_node.parent)
				_jm.remove_node(selected_node)
			}
		},
		handle_toggle: function (_jm, e) {
			var evt = e || event
			var selected_node = _jm.get_selected_node()
			if (!!selected_node) {
				_jm.toggle_node(selected_node.id)
				evt.stopPropagation()
				evt.preventDefault()
			}
		},
		handle_up: function (_jm, e) {
			var evt = e || event
			var selected_node = _jm.get_selected_node()
			if (!!selected_node) {
				var up_node = _jm.find_node_before(selected_node)
				if (!up_node) {
					var np = _jm.find_node_before(selected_node.parent)
					if (!!np && np.children.length > 0) {
						up_node = np.children[np.children.length - 1]
					}
				}
				if (!!up_node) {
					_jm.select_node(up_node)
				}
				evt.stopPropagation()
				evt.preventDefault()
			}
		},

		handle_down: function (_jm, e) {
			var evt = e || event
			var selected_node = _jm.get_selected_node()
			if (!!selected_node) {
				var down_node = _jm.find_node_after(selected_node)
				if (!down_node) {
					var np = _jm.find_node_after(selected_node.parent)
					if (!!np && np.children.length > 0) {
						down_node = np.children[0]
					}
				}
				if (!!down_node) {
					_jm.select_node(down_node)
				}
				evt.stopPropagation()
				evt.preventDefault()
			}
		},

		handle_left: function (_jm, e) {
			this._handle_direction(_jm, e, jm.direction.left)
		},
		handle_right: function (_jm, e) {
			this._handle_direction(_jm, e, jm.direction.right)
		},
		_handle_direction: function (_jm, e, d) {
			var evt = e || event
			var selected_node = _jm.get_selected_node()
			var node = null
			if (!!selected_node) {
				if (selected_node.isroot) {
					var c = selected_node.children
					var children = []
					for (var i = 0; i < c.length; i++) {
						if (c[i].direction === d) {
							children.push(i)
						}
					}
					node = c[children[Math.floor((children.length - 1) / 2)]]
				} else if (selected_node.direction === d) {
					var children = selected_node.children
					var childrencount = children.length
					if (childrencount > 0) {
						node = children[Math.floor((childrencount - 1) / 2)]
					}
				} else {
					node = selected_node.parent
				}
				if (!!node) {
					_jm.select_node(node)
				}
				evt.stopPropagation()
				evt.preventDefault()
			}
		}
	}

	// plugin
	jm.plugin = function (name, init) {
		this.name = name
		this.init = init
	}

	jm.plugins = []

	jm.register_plugin = function (plugin) {
		if (plugin instanceof jm.plugin) {
			jm.plugins.push(plugin)
		}
	}

	jm.init_plugins = function (sender) {
		$w.setTimeout(function () {
			jm._init_plugins(sender)
		}, 0)
	}

	jm._init_plugins = function (sender) {
		var l = jm.plugins.length
		var fn_init = null
		for (var i = 0; i < l; i++) {
			fn_init = jm.plugins[i].init
			if (typeof fn_init === 'function') {
				fn_init(sender)
			}
		}
	}

	// quick way
	jm.show = function (options, mind) {
		var _jm = new jm(options)
		_jm.show(mind)
		return _jm
	}

	// export jsmind
	if (typeof module !== 'undefined' && typeof exports === 'object') {
		module.exports = jm
	} else if (typeof define === 'function' && (define.amd || define.cmd)) {
		define(function () {
			return jm
		})
	} else {
		$w[__name__] = jm
	}
})(typeof window !== 'undefined' ? window : global)
